import { NodeValue, NodeInfo } from './types';

let nodeValues: NodeValue[] = [];
let idMap = new WeakMap<Node, number>();
let index = 1;
function getId(node: Node, autogen: boolean = false): number {
  if (node === null) {
    return null;
  }
  let id = idMap.get(node);
  if (!id && autogen) {
    id = index++;
    idMap.set(node, id);
  }

  return id ? id : null;
}

function getPreviousId(node: Node): number {
  let id = null;

  // Some nodes may not have an ID by design since Clarity skips over tags like SCRIPT, NOSCRIPT, META, COMMENTS, etc..
  // In that case, we keep going back and check for their sibling until we find a sibling with ID or no more sibling nodes are left.
  while (id === null && node.previousSibling) {
    id = getId(node.previousSibling);
    node = node.previousSibling;
  }
  return id;
}

function reset() {
  idMap = new WeakMap<Node, number>();
  index = 1;
}

function getAttributes(element: Element): Record<string, string> {
  const attrs: Record<string, string> = {};
  const attributes = element.attributes;

  for (let i = 0; i < attributes.length; i++) {
    const attr = attributes[i];
    attrs[attr.name] = attr.value;
  }

  return attrs;
}

function traverse(root: Node) {
  const queue = [root];
  while (queue.length > 0) {
    const node = queue.shift()!;
    const next = node.firstChild;

    // 收集所有子节点
    let sibling = next;
    while (sibling) {
      queue.push(sibling);
      sibling = sibling.nextSibling;
    }

    const nodeInfo: NodeInfo = { tag: '' };

    switch (node.nodeType) {
      case Node.ELEMENT_NODE:
        const element = node as Element;

        nodeInfo.tag = element.tagName;
        nodeInfo.attributes = getAttributes(element);

        switch (element.tagName) {
          case 'HTML':
            // todo 当HTML节点在iframe中时,需要额外处理，暂时不处理
            addNodeValue(node, nodeInfo);
            break;
          case 'SCRIPT':
            // 脚本节点不做处理
            break;
          case 'NOSCRIPT':
            // 保留 noscript 标签但忽略其内容。一些 HTML 标记依赖于这些标签来保持与原始 CSS 视图的一致性，
            // 但我们不希望在 回放时JS必然是启动的，无需执行 noscript 中的内容，无需收集其内容
            nodeInfo.value = '';
            addNodeValue(node, nodeInfo);
            break;
          case 'META':
            // META 标签不做处理
            break;
          case 'HEAD':
            let l = location;
            nodeInfo.attributes['*B'] = l.protocol + '//' + l.host + l.pathname;
            addNodeValue(node, nodeInfo);
            break;
          case 'BASE':
            // 覆盖自动检测的基础路径，使用此标签中指定的显式值
            const id = getId(node.parentElement as Node);
            let baseHead = nodeValues.find((n) => n.id === id);
            if (baseHead) {
              // 创建一个 "a" 元素，以便为相对路径（如 "/path/"）生成协议和主机名
              let a = document.createElement('a');
              a.href = nodeInfo.attributes['href'];
              const baseHeadNodeInfo = baseHead.data;
              if (baseHeadNodeInfo.attributes) {
                baseHeadNodeInfo.attributes['*B'] = a.protocol + '//' + a.host + a.pathname;
              }
            }
            break;
          case 'STYLE':
            nodeInfo.value = getStyleValue(element as HTMLStyleElement);
            addNodeValue(node, nodeInfo);
            break;
          case 'LINK':
            // todo electron要特殊处理，暂时不处理
            addNodeValue(node, nodeInfo);
            break;
          case 'IFRAME':
            // 忽略 iframe 节点
            break;
          case 'VIDEO':
          case 'AUDIO':
          case 'SOURCE':
            if ('src' in nodeInfo.attributes && nodeInfo.attributes['src'].startsWith('data:')) {
              nodeInfo.attributes['src'] = '';
            }
            addNodeValue(node, nodeInfo);
            break;
          default:
            addNodeValue(node, nodeInfo);
            break;
        }

        break;

      case Node.TEXT_NODE:
        nodeInfo.tag = '*T';
        nodeInfo.value = node.nodeValue ?? undefined;

        addNodeValue(node, nodeInfo);
        break;
      case Node.DOCUMENT_TYPE_NODE:
        const doctype = node as DocumentType;
        nodeInfo.tag = '*D';
        nodeInfo.attributes = {
          name: doctype.name,
          publicId: doctype.publicId,
          systemId: doctype.systemId,
        };

        addNodeValue(node, nodeInfo);
        break;
      case Node.DOCUMENT_NODE:
        // 文档节点不做处理
        break;
      case Node.DOCUMENT_FRAGMENT_NODE:
        // 文档片段节点不做处理
        break;
    }
  }
}

function getStyleValue(style: HTMLStyleElement): string {
  // Call trim on the text content to ensure we do not process white spaces ( , \n, \r\n, \t, etc.)
  // Also, check if stylesheet has any data-* attribute, if so process rules instead of looking up text
  // Additionally, check if style node has an id - if so it's at a high risk to have experienced dynamic
  // style updates which would make the textContent out of date with its true style contribution.
  let value = style.textContent ? style.textContent.trim() : '';
  let dataset = style.dataset ? Object.keys(style.dataset).length : 0;
  if (value.length === 0 || dataset > 0 || style.id.length > 0) {
    value = getCssRules(style.sheet as CSSStyleSheet);
  }
  return value;
}

function getCssRules(sheet: CSSStyleSheet): string {
  let value = '' as string;
  let cssRules = null;
  // Firefox throws a SecurityError when trying to access cssRules of a stylesheet from a different domain
  try {
    cssRules = sheet ? sheet.cssRules : [];
  } catch (e) {
    if (e && e.name !== 'SecurityError') {
      throw e;
    }
  }

  if (cssRules !== null) {
    for (let i = 0; i < cssRules.length; i++) {
      value += cssRules[i].cssText;
    }
  }

  return value;
}

function addNodeValue(node: Node, nodeInfo: NodeInfo) {
  nodeValues.push({
    id: getId(node, true),
    type: node.nodeType,
    parent: node.parentNode ? getId(node.parentNode) : undefined,
    previous: node.previousSibling ? getPreviousId(node) : undefined,
    data: nodeInfo,
  });
}

/**
 * 捕获当前 DOM 树的快照
 */
export function snapshot() {
  reset();
  nodeValues = [];
  traverse(document);
  return nodeValues;
}

export type { NodeValue };
